0%

Event Loop

介绍一下事件循环

宏任务和微任务

前提

JS是单线程的,JS是通过事件队列(Event Loop)的方式来实现异步回调的,怎么拥有的,下面我们从进程、线程的角度来解释

CPU

计算机的核心是CPU,它承担了所有的计算任务。

它就像一座工厂,时刻在运行。

进程

进程就好比工厂的车间,它代表CPU所能处理的单个任务。 进程之间相互独立,任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。 CPU使用时间片轮转进度算法来实现同时运行多个进程。

线程

一个车间里,可以有很多工人,共享车间所有的资源,他们协同完成一个任务。

线程就好比车间里的工人,一个进程可以包括多个线程,多个线程共享进程资源。

CPU、进程、线程之间的关系
  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
  • 不同进程之间也可以通信,不过代价较大
  • 单线程与多线程,都是指在一个进程内的单和多
浏览器是多进程的
  • 浏览器是都进程的
  • 每个Tab页,就是一个独立的进程
浏览器包含哪些进程
  • 主进程

    • 协调控制其他子进程(创建、销毁)
    • 浏览器界面显示,用户交互,前进,后退,收藏
    • 将渲染进程得到的内存中Bitmap,绘制到用户界面上
    • 处理不可见操作,网络请求,文件访问等
  • 第三方插件进程

    • 每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU进程

    • 用于3D绘制等
  • 渲染进程,就是我们说的浏览器内核

    • 负责页面渲染,脚本执行,事件处理等
    • 每个tab页一个渲染进程
浏览器内核(渲染进程)
  • GUI渲染线程

    • 负责渲染页面,布局和绘制
    • 页面需要重绘和回流时,该线程就会执行
    • 与js引擎线程互斥,防止渲染结果不可预期
  • JS引擎线程

    • 负责处理解析和执行javascript脚本程序
    • 只有一个JS引擎线程(单线程)
    • 与GUI渲染线程互斥,防止渲染结果不可预期
  • 事件触发线程

    • 用来控制事件循环(鼠标点击,setTimeOut, ajax等)
    • 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中
  • 定时触发线程

    • setInterval与setTimeout所在的线程
    • 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
    • 计时完毕后,通知事件触发线程
  • 异步http请求线程

    • 浏览器有一个单独的线程用来处理AJAX请求
    • 当请求完时,若由回调函数,通知事件触发线程
为什么javascript是单线程的

首先是历史原因,在创建 javascript 这门语言时,多进程多线程的架构并不流行,硬件支持并不好。

其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高

作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质

为什么GUI渲染进程和JS引擎线程互斥

因为JS可以操作DOM,如果改变元素属性并同时渲染界面(JS线程和UI线程同时运行), 那么渲染线程前后获得的元素就不一致了

因此,为了防止渲染出现不可预期的结果,浏览器设定GUI渲染进程和JS引擎线程为互斥关系, 当JS引擎线程执行时GUI渲染线程会被挂起,GUI跟新则会被保存在一个队列中等待JS引擎线程空闲时立即被执行

从Event Loop看JS的运行机制

BHnUhj.png

  • js分为同步任务和异步任务
  • 同步任务都在JS引擎线程上执行,形成一个执行栈
  • 事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中
  • 执行栈中所有的同步任务执行完毕时,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行
  1. 我们知道,不管是setTimeout/setInterval和XHR/fetch代码,在这些代码执行时,
    本身是同步任务,而其中的回调函数才是异步任务。

  2. 当代码执行到setTimeout/setInterval时,实际上是JS引擎线程通知定时触发器线程,间隔一个时间后,会触发一个回调事件,而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中。

  3. 当代码执行到XHR/fetch时,实际上是JS引擎线程通知异步http请求线程,发送一个网络请求,并制定请求完成后的回调事件,而异步http请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中。

  4. 当我们的同步任务执行完,JS引擎线程会询问事件触发线程,在事件队列中是否有待执行的回调函数,如果有就会加入到执行栈中交给JS引擎线程执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let timerCallback = function() {
console.log('wait one second');
};
let httpCallback = function() {
console.log('get server data success');
}

// 同步任务
console.log('hello');
// 同步任务
// 通知定时器线程 1s 后将 timerCallback 交由事件触发线程处理
// 1s 后事件触发线程将 timerCallback 加入到事件队列中
setTimeout(timerCallback,1000);
// 同步任务
// 通知异步http请求线程发送网络请求,请求成功后将 httpCallback 交由事件触发线程处理
// 请求成功后事件触发线程将 httpCallback 加入到事件队列中
$.get('www.xxxx.com',httpCallback);
// 同步任务
console.log('world');
//...
// 所有同步任务执行完后
// 询问事件触发线程在事件事件队列中是否有需要执行的回调函数
// 如果没有,一直询问,直到有为止
// 如果有,将回调事件加入执行栈中,开始执行回调代码

总结一下:

  • JS引擎线程只执行执行栈中的事件
  • 执行栈中的代码执行完毕,就会读取事件队列中的事件
  • 事件队列中的回调事件,是由各自线程插入到事件队列中的
  • 如此循环
宏任务

我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。

我们前文提到过JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染

1
// 宏任务-->渲染-->宏任务-->渲染-->渲染...

主代码块,setTimeout,setInterval等,都属于宏任务

1
2
3
4
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';

我们会看到的结果是,页面背景会在瞬间变成灰色,以上代码属于同一次宏任务,所以全部执行完才触发页面渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉效果上,只会看到页面变成灰色。

1
2
3
4
document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black'
},0)

我会看到,页面先显示成蓝色背景,然后瞬间变成了黑色背景,这是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色。

微任务

微任务可以理解为在当前宏任务执行后立即执行的任务

也就是说,当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完

Promise、process.nextTick等,属于微任务

1
2
3
4
5
6
7
8
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:black'
});
console.log(3);

输出 1, 3, 2, 页面直接变成黑色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})

1, 7, 6, 8, 2, 4, 3, 5, 9, 11, 10, 12

通过自己画执行任务队列,分宏任务,微任务,
每次执行完一个宏任务,去执行所有微任务,

微任务嵌套宏任务时,等到执行那个微任务,再将宏任务加入执行队列里面,每次只看最外层是个什么任务,当作一个整体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
setTimeout(function() {
console.log('aa')
})
resolve();
}).then(function() {
console.log('5')
setTimeout(function() {
console.log('bb')
})
})
})

2,4, 3, aa, bb